App.require(["matrix4", "vector3", "vector4"], function (matrix4, vector3, vector4) {
    var Matrix4 = matrix4.Matrix4,
        Vector4 = vector4.Vector4,
        Vector3 = vector3.Vector3;

    function PhotographerCamera() {
        this.position = new Vector3(0, 0, 0);
        this.orientation = new Matrix4();
        this.t_V = new Vector3(0, 0, 0);
        this.delta = new Vector3(0, 0, 0);
        this.orienting_view = false;
        this.lockToCar = false;
        this.start_x = 0;
        this.start_y = 0;

        var me = this;
        this.handleKey = {};
        this.handleKey["Q"] = function () {
            me.t_V.set(0, 0.1, 0);
        };
        this.handleKey["E"] = function () {
            me.t_V.set(0, -0.1, 0);
        };
        this.handleKey["L"] = function () {
            me.lockToCar = true;
        };
        this.handleKey["U"] = function () {
            me.lockToCar = false;
        };

        this.keyDown = function (keyCode) {
            if (this.handleKey[keyCode]) this.handleKey[keyCode](true);
        }

        this.keyUp = function (keyCode) {
            this.delta.set(0, 0, 0);
        }

        this.mouseMove = function (x, y) {
            if (!this.orienting_view) return;

            var alpha = (x - this.start_x) / 10.0;
            var beta = -(y - this.start_y) / 10.0;
            this.start_x = x;
            this.start_y = y;

            var R_alpha = Matrix4.fromRotationAxis({
                x: 0,
                y: 1,
                z: 0
            }, alpha * Math.PI / 180);
            var R_beta = Matrix4.fromRotationAxis({
                x: 1,
                y: 0,
                z: 0
            }, beta * Math.PI / 180);
            this.orientation.premultiply(R_alpha).multiply(R_beta);
        };

        this.mouseButtonDown = function (x, y) {
            if (!this.lock_to_car) {
                this.orienting_view = true;
                this.start_x = x;
                this.start_y = y;
            }
        };

        this.mouseButtonUp = function () {
            this.orienting_view = false;
        }

        this.updatePosition = function (t_V) {
            this.position.add(t_V.clone().applyMatrix4(this.orientation));
            if (this.position.y > 1.8) this.position.y = 1.8;
            if (this.position.y < 0.5) this.position.y = 0.5;
        }

        this.setView = function (stack, carFrame) {
            this.updatePosition(this.t_V)
            var car_position = Vector3.fromMatrixColumn(carFrame, 3);
            if (this.lockToCar)
                var invV = Matrix4.lookAt(this.position, car_position, {
                    x: 0,
                    y: 1,
                    z: 0
                });
            else
                var invV = Matrix4.lookAt(this.position, 
                    Vector3.sub(this.position, Vector3.fromMatrixColumn(this.orientation, 2)), 
                    Vector3.fromMatrixColumn(this.orientation, 1));
            stack.multiply(invV);
        };
    };

    function ChaseCamera() {
        this.position = new Vector3();
        this.keyDown = function (keyCode) {}
        this.keyUp = function (keyCode) {}
        this.mouseMove = function (event) {};
        this.mouseButtonDown = function (event) {};
        this.mouseButtonUp = function () {}
        this.setView = function (stack, F_0) {
            var rx = Matrix4.fromRotationAxis({
                x: 1.0,
                y: 0.0,
                z: 0.8
            }, Math.PI * -15 / 180);
            var t = Matrix4.fromTranslation(0, 2.5, 4.5);
            var vc_0 = t.multiply(rx)
            var v_0 = Matrix4.multiply(F_0, vc_0);
            this.position = Vector3.fromMatrixColumn(v_0, 3);
            var invV = Matrix4.fromInverse(v_0);
            stack.multiply(invV);
        };
    };

    function ObserverCamera() {
        this.currentMode = 0;
        this.V = new Matrix4().setPosition(new Vector3(0, 20, 100));
        this.position = new Vector3();
        this.t_V = new Vector4(0, 0, 0, 0.0);
        this.alpha = 0;
        this.beta = 0;

        this.height = 0;
        this.width = 0;
        this.start_x = 0;
        this.start_y = 0;
        this.currX = 0;
        this.currY = 0;
        this.rad = 5;
        this.orbiting = false;
        this.projectionMatrix = new Matrix4();
        this.rotMatrix = new Matrix4();
        this.tbMatrix = new Matrix4();

        this.computeVector = function (x, y) {
            var D = 2 * this.rad;
            var xf = (x - this.width / 2) / this.width * 2.0;
            var yf = (y - this.height / 2) / this.height * 2.0;

            var invProjection = Matrix4.fromInverse(this.projectionMatrix);
            var v = new Vector4(xf, yf, -1, 1).applyMatrix4(invProjection);
            v.multiplyScalar(1 / v.w);

            var h = Math.sqrt(v.x * v.x + v.y * v.y);

            var a = v.x * v.x + v.y * v.y + v.z * v.z;
            var b = 2 * D * v.y;
            var c = D * D - this.rad * this.rad;

            var discriminant = b * b - 4 * a * c, t, t1;
            if (discriminant > 0) {
                t = (-b - Math.sqrt(discriminant)) / (2 * a);
                t1 = (-b + Math.sqrt(discriminant)) / (2 * a);
                if (t < 0) t = 100 * this.rad;
                if (t1 < 0) t1 = 100 * this.rad;
                if (t1 < t) t = t1;

                if (t * v.x * t * v.x + t * v.y * t * v.y < this.rad * this.rad / 2)
                    return [t * v.x, t * v.y, t * v.z + D];
            }

            a = 2 * v.z * h;
            b = 2 * D * h;
            c = -this.rad * this.rad;

            discriminant = b * b - 4 * a * c;
            t = (-b - Math.sqrt(discriminant)) / (2 * a);
            t1 = (-b + Math.sqrt(discriminant)) / (2 * a);
            if (t < 0) t = 0;
            if (t1 < 0) t1 = 0;
            if (t < t1) t = t1;

            return new Vector3(t * v.x, t * v.y, t * v.z + D);

        };

        this.updateCamera = function () {
            if (this.currentMode == 1)
                return;

            var dir_world = this.t_V.clone().applyMatrix4(this.V);
            var newPosition = Vector4.fromMatrixColumn(this.V, 3).add(dir_world);

            this.V.setPosition({ x: 0, y: 0, z: 0 });
            var R_alpha = Matrix4.fromRotationAxis({x: 0, y: 1, z: 0}, this.alpha * Math.PI / 1800);
            var R_beta = Matrix4.fromRotationAxis({x: 1, y: 0, z: 0}, this.beta * Math.PI / 1800);
            this.V.premultiply(R_alpha).multiply(R_beta);
            this.V.setPosition(newPosition);
            this.position = newPosition;
            this.alpha = 0;
            this.beta = 0;
        };

        this.forward = function (on) {
            this.t_V.set(0, 0, -on / 1.0, 0.0);
        };
        this.backward = function (on) {
            this.t_V.set(0, 0, on / 1.0, 0.0);
        };
        this.left = function (on) {
            this.t_V.set(-on / 1.0, 0, 0, 0.0);
        };
        this.right = function (on) {
            this.t_V.set(on / 1.0, 0, 0, 0.0);
        };
        this.up = function (on) {
            this.t_V.set(0.0, on / 3.0, 0, 0.0);
        };
        this.down = function (on) {
            this.t_V.set(0.0, -on / 3.0, 0, 0.0);
        };

        me = this;
        this.handleKeyObserver = {};
        this.handleKeyObserver["W"] = function (on) {
            me.forward(on);
        };
        this.handleKeyObserver["S"] = function (on) {
            me.backward(on);
        };
        this.handleKeyObserver["A"] = function (on) {
            me.left(on);
        };
        this.handleKeyObserver["D"] = function (on) {
            me.right(on);
        };
        this.handleKeyObserver["Q"] = function (on) {
            me.up(on);
        };
        this.handleKeyObserver["E"] = function (on) {
            me.down(on);
        };

        this.handleKeyObserver["M"] = function (on) {
            me.currentMode = 1;
        };
        this.handleKeyObserver["N"] = function (on) {
            me.currentMode = 0;
        };

        this.keyDown = function (keyCode) {
            this.handleKeyObserver[keyCode] && this.handleKeyObserver[keyCode](true);
        };
        this.keyUp = function (keyCode) {
            this.handleKeyObserver[keyCode] && this.handleKeyObserver[keyCode](false);
        };

        this.mouseButtonDown = function (x, y) {
            if (this.currentMode == 0) {
                this.aiming = true;
                this.start_x = x;
                this.start_y = y;
            } else {
                this.currX = x;
                this.currY = y;
                this.orbiting = true;
            }
        };
        this.mouseButtonUp = function (event) { 
            if (this.orbiting) {
                var invTbMatrix = Matrix4.fromInverse(this.tbMatrix);
                this.V.premultiply(invTbMatrix);
                this.tbMatrix = new Matrix4();
                this.rotMatrix = new Matrix4();
                this.orbiting = false;
            } else
                this.aiming = false;
        };
        this.mouseMove = function (x, y) {
            if (this.currentMode == 0) {
                if (this.aiming) {
                    this.alpha = x - this.start_x;
                    this.beta = -(y - this.start_y);
                    this.start_x = x;
                    this.start_y = y;
                    this.updateCamera();
                }
                return;
            }

            if (!this.orbiting) return;

            var newX = x;
            var newY = y;

            var p0_prime = this.computeVector(this.currX, this.currY);
            var p1_prime = this.computeVector(newX, newY);

            var axis = Vector3.cross(p0_prime, p1_prime);
            var axis_length = Vector3.sub(p0_prime, p1_prime).length();
            var angle = axis_length / this.rad;
            angle = p0_prime.angleTo(p1_prime);
            if (angle > 0.00001) {
                this.rotMatrix.premultiply(Matrix4.fromRotationAxis(axis.applyMatrix4(this.V), angle));
            }

            var dir_world = Vector3.fromMatrixColumn(this.V, 2).multiplyScalar(-2 * this.rad);
            var tbCenter = Vector3.add(Vector3.fromMatrixColumn(this.V, 3), dir_world);

            var tMatrixInv = Matrix4.fromTranslation(tbCenter.x, tbCenter.y, tbCenter.z);
            var tMatrix = Matrix4.fromTranslation(-tbCenter.x, -tbCenter.y, -tbCenter.z);

            Matrix4.multiply(tMatrixInv, this.rotMatrix, this.tbMatrix).multiply(tMatrix);

            this.currX = newX;
            this.currY = newY;
        }
        this.setView = function (stack) {
            this.updateCamera();
            var invV = Matrix4.fromInverse(this.V);
            stack.multiply(invV).multiply(this.tbMatrix);
        }
    };

    function DriverCamera() {
        this.position = [];
        this.keyDown = function (keyCode) {}
        this.keyUp = function (keyCode) {}
    
        this.mouseMove = function (event) {};
    
        this.mouseButtonDown = function (event) {};
    
        this.mouseButtonUp = function () {}
    
        this.setView = function (stack, frame) {
            var driverFrame = Matrix4.copyFrom(frame);
            var pos = Vector4.fromMatrixColumn(driverFrame, 3).add(new Vector4(0, 1.5, 0, 0));
            driverFrame.setPosition(pos);
            var invV = Matrix4.fromInverse(driverFrame);
            stack.multiply(invV);
        };
    };

    App.define("camera", {
        PhotographerCamera: PhotographerCamera,
        ChaseCamera: ChaseCamera,
        ObserverCamera: ObserverCamera,
        DriverCamera: DriverCamera
    })
})